JavaScript 中 this 的绑定

关于 this

this 关键字是 JavaScript 中最复杂的机制之一,它是一个很特别的关键字,被自动定义在所有函数的作用域中。

this 提供了一种更优雅的方式来隐式『传递』一个对象的引用,因此可以将 API 设计得更加简洁并且易于复用。

注意:

1.this 不指向自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
this.count++;
}
foo.count = 0;

var i;

for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// foo 被调用了多少次?
console.log( foo.count ); // 0

以上代码显然没有按照我们预期的效果运行,问题就出在 this 并不指向自身上,以下是修改的方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
// 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以确保 this 指向函数对象 foo 本身
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// foo 被调用了多少次?
console.log( foo.count ); // 4

使用 call 方法强制绑定 this 指向 foo,解决了之前的问题。

2.this 不代表它的作用域

在某种情况下,this 指向函数的作用域,但有时又不是。

this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域『对象』无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。

3.this 到底是什么

this 是在运行时进行绑定的,并不是在编写时,它的上下文取决于函数调用时的各种条件。所以,this 的绑定和声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。

调用位置

只有清晰地了解了调用位置,才能明白 this 到底引用的是什么。

调用位置是函数在代码中被调用的位置(而不是声明的位置)。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

下面通过代码展示什么是调用栈和调用位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域

console.log( "baz" );
bar(); // <-- bar 的调用位置
}

function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中

console.log( "bar" );
foo(); // <-- foo 的调用位置
}

function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}

baz(); // <-- baz 的调用位置

绑定规则

1.默认绑定

2.隐式绑定

对象属性引用链中只有最顶层或者说最后一层会影响调用位置

1
2
3
4
5
6
7
8
9
10
function foo() { 
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

obj.foo(); // 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() { 
console.log( this.a );
}

var obj2 = {
a: 42,
foo: foo
};

var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

隐式丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() { 
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

var bar = obj.foo; // 函数别名!

var a = "oops, global"; // a 是全局对象的属性

bar(); // "oops, global"

3.显式绑定

可以使用函数的 call(..)apply(..) 方法。

4.new 绑定

包括内置对象函数在内的所有函数都可以用 new 来调用,这种函数调用被称为构造函数调用。

使用 new 来调用函数,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行『原型』连接。
  3. 这个新对象会绑定到函数调用的 this
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象